﻿using System;
using System.Text.RegularExpressions;

namespace NoSeam.AudioTools.CueSheet
{
	/// <summary>
	/// Represents an M:S:F timecode specifying the gap length or index offset within a track.
	/// </summary>
	public struct Timecode : IEquatable<Timecode>, IComparable<Timecode>
	{
		/// <summary>
		/// The number of frames in a second.
		/// </summary>
		public const int FramesPerSecond = 75;

		/// <summary>
		/// The number of seconds in a minute.
		/// </summary>
		public const int SecondsPerMinute = 60;

		/// <summary>
		/// Convenience access to a Timecode with zero offset.
		/// </summary>
		public static readonly Timecode Zero = new Timecode();

		/// <summary>
		/// The pattern used in the Parse() method.
		/// </summary>
		public static readonly Regex Pattern = new Regex(@"\A(?<m>\d+):(?<s>\d+):(?<f>\d+)\z");

		#region Constructors

		/// <summary>
		/// Creates new value from the number of frames.
		/// </summary>
		/// <param name="offset">The offset specified in frames.</param>
		public Timecode(int offset) : this()
		{
			this.Offset = offset;
		}

		/// <summary>
		/// Creates new value from the individual M:S:F parts.
		/// <para/>
		/// Parts can be out of range (for example, seconds can be negative or greater than 59)
		/// as long as the resulting offset is valid.
		/// <para/>
		/// The offset is calculated as (M * 60 + S) * 75 + F.
		/// </summary>
		/// <param name="minutes">The number of minutes.</param>
		/// <param name="seconds">The number of seconds.</param>
		/// <param name="frames">The number of frames.</param>
		public Timecode(int minutes, int seconds, int frames) : this()
		{
			this.Offset = ((minutes * Timecode.SecondsPerMinute) + seconds) * Timecode.FramesPerSecond + frames;
		}

		/// <summary>
		/// Creates new value from a string.
		/// </summary>
		/// <param name="value">
		/// The colon-separated minutes, seconds, and frames, such as "0:0:0" or "0:60:200".
		/// <para/>
		/// Each part is one or more decimal digits. All parts must be present.
		/// </param>
		/// <returns>New Timecode value.</returns>
		public static Timecode Parse(string value)
		{
			if (value == null)
			{
				throw new ArgumentNullException("Timecode string cannot be null.");
			}

			Match match = Timecode.Pattern.Match(value);
			if (!match.Success)
			{
				throw new ArgumentException("Timecode string is malformed.", value);
			}

			int minutes = int.Parse(match.Groups["m"].Value);
			int seconds = int.Parse(match.Groups["s"].Value);
			int frames = int.Parse(match.Groups["f"].Value);

			return new Timecode(minutes, seconds, frames);
		}

		#endregion

		#region The "Offset" property

		int offset;

		/// <summary>
		/// Timecode value as a total number of frames, which is (M * 60 + S) * 75 + F.
		/// <para/>
		/// It can be set to an arbitrarily large value, but cannot be negative.
		/// </summary>
		public int Offset
		{
			get
			{
				return offset;
			}

			set
			{
				if (value < 0)
				{
					throw new ArgumentOutOfRangeException("value", value, "Negative offset is not allowed.");
				}
				offset = value;
			}
		}

		#endregion

		#region Individual accessors to M:S:F parts

		/// <summary>
		/// The "minutes" part.
		/// </summary>
		public int Minutes
		{
			get
			{
				int numSeconds = offset / Timecode.FramesPerSecond;
				return numSeconds / Timecode.SecondsPerMinute;
			}
		}

		/// <summary>
		/// The "seconds" part in the range 0..59 (inclusive).
		/// </summary>
		public int Seconds
		{
			get
			{
				int numSeconds = offset / Timecode.FramesPerSecond;
				return numSeconds % Timecode.SecondsPerMinute;
			}
		}

		/// <summary>
		/// The "frames" part in the range 0..74 (inclusive).
		/// </summary>
		public int Frames
		{
			get
			{
				return offset % Timecode.FramesPerSecond;
			}
		}

		#endregion

		#region Operators

		/// <summary>
		/// Type specific equality operator.
		/// </summary>
		/// <returns>true if both Timecodes represent the same offset; otherwise, false.</returns>
		public static bool operator ==(Timecode left, Timecode right)
		{
			return left.offset == right.offset;
		}

		/// <summary>
		/// Type specific inequality operator.
		/// </summary>
		/// <returns>true if the specified Timecodes represent different offsets; otherwise, false.</returns>
		public static bool operator !=(Timecode left, Timecode right)
		{
			return !(left == right);
		}

		/// <summary>Type specific "less than" operator.</summary>
		public static bool operator <(Timecode left, Timecode right)
		{
			return left.offset < right.offset;
		}

		/// <summary>Type specific "less than or equal" operator.</summary>
		public static bool operator <=(Timecode left, Timecode right)
		{
			return !(right < left);
		}

		/// <summary>Type specific "greater than" operator.</summary>
		public static bool operator >(Timecode left, Timecode right)
		{
			return right < left;
		}

		/// <summary>Type specific "greater than or equal" operator.</summary>
		public static bool operator >=(Timecode left, Timecode right)
		{
			return !(left < right);
		}

		#endregion

		#region Object overrides

		/// <summary>
		/// Overrides the Object.Equals().
		/// </summary>
		/// <param name="obj">The object to compare with the current object.</param>
		/// <returns>
		/// true if the specified object is a Timecode and its Offset is equal to that of the current object;
		/// otherwise, false.
		/// </returns>
		public override bool Equals(object obj)
		{
			return obj is Timecode && this == (Timecode)obj;
		}

		/// <summary>
		/// Overrides the Object.GetHashCode().
		/// </summary>
		/// <returns>A hash code for the current object.</returns>
		public override int GetHashCode()
		{
			return offset;
		}

		/// <summary>
		/// Overrides the Object.ToString().
		/// </summary>
		/// <returns>
		/// Colon-separated M:S:F string, with normalized parts.
		/// <para/>
		/// Frames go zero-padded from 00 up to 74, and seconds go from 00 up to 59.
		/// <para/>
		/// Minutes go from 00 up, adding more digits as necessary.
		/// </returns>
		public override string ToString()
		{
			return string.Format("{0:00}:{1:00}:{2:00}", this.Minutes, this.Seconds, this.Frames);
		}

		#endregion

		#region Interface implementations

		/// <summary>
		/// Implements IEquatable.Equals().
		/// </summary>
		/// <param name="other">The value to compare with the current value.</param>
		/// <returns>
		/// true if the specified value's Offset is equal to that of the current value;
		/// otherwise, false.
		/// </returns>
		public bool Equals(Timecode other)
		{
			return this == other;
		}

		/// <summary>
		/// Implements IComparable.CompareTo().
		/// </summary>
		/// <param name="other">The value to compare with the current value.</param>
		/// <returns>
		/// A value that indicates the relative order of the objects being compared.
		/// <para/>
		/// Negative: "this" is less than "other".
		/// <para/>
		/// Zero: "this" is equal to "other".
		/// <para/>
		/// Positive: "this" is greater than "other".
		/// </returns>
		public int CompareTo(Timecode other)
		{
			return offset - other.offset;
		}

		#endregion
	}
}
